import SEO from "../components/SEO";
import { TOC, TOCList, TOCLink } from "../components/TOC";
import { Pipe } from "../components/Pipe";

<SEO title="Tabs" description="Accessible tabs component for React" />

# Tabs

<TOC>
	<TOCList>
		<TOCLink href="#tabs-1">Tabs</TOCLink>
		<TOCLink href="#tablist">TabList</TOCLink>
		<TOCLink href="#tab">Tab</TOCLink>
		<TOCLink href="#tabpanels">TabPanels</TOCLink>
		<TOCLink href="#tabpanel">TabPanel</TOCLink>
		<TOCLink href="#usetabscontext">useTabsContext</TOCLink>
	</TOCList>
</TOC>

- Source: https://github.com/reach/reach-ui/tree/main/packages/tabs
- WAI-ARIA: https://www.w3.org/TR/wai-aria-practices-1.2/#tabpanel

An accessible tabs component.

The `Tab` and `TabPanel` elements are associated by their order in the tree. None of the components are empty wrappers, each is associated with a real DOM element in the document, giving you maximum control over styling and composition.

You can render any other elements you want inside of `Tabs`, but `TabList` should only render `Tab` elements, and `TabPanels` should only render `TabPanel` elements.

```jsx
// jsx-demo
function Example() {
	return (
		<Tabs>
			<TabList>
				<Tab>One</Tab>
				<Tab>Two</Tab>
				<Tab>Three</Tab>
			</TabList>

			<TabPanels>
				<TabPanel>
					<p>one!</p>
				</TabPanel>
				<TabPanel>
					<p>two!</p>
				</TabPanel>
				<TabPanel>
					<p>three!</p>
				</TabPanel>
			</TabPanels>
		</Tabs>
	);
}
```

[Check out the demos](#demos) for ideas on how to style and compose.

## Installation

From the command line in your project directory, run `npm install @reach/tabs` or `yarn add @reach/tabs`. Then import the components and styles that you need:

```bash
npm install @reach/tabs
# or
yarn add @reach/tabs
```

```js
import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@reach/tabs";
import "@reach/tabs/styles.css";
```

## Component API

### Tabs

The parent component of the tab interface.

#### Tabs CSS Selectors

Please see the [styling guide](/styling).

```css
[data-reach-tabs] {
}
```

#### Tabs Props

| Prop                                             | Type                               | Required | Default        |
| ------------------------------------------------ | ---------------------------------- | -------- | -------------- |
| [`as`](#tabs-as)                                 | `string` <Pipe /> `Component`      | false    | `"div"`        |
| [`children`](#tabs-children)                     | `node`                             | true     |                |
| [`defaultIndex`](#tabs-defaultindex)             | `number`                           | false    |                |
| [`index`](#tabs-index)                           | `number`                           | false    |                |
| [`keyboardActivation`](#tabs-keyboardactivation) | `"auto"` <Pipe /> `"manual"`)      | false    | `"auto`        |
| [`onChange`](#tabs-onchange)                     | `func`                             | false    |                |
| [`orientation`](#tabs-orientation)               | `"horizontal"`<Pipe />`"vertical"` | false    | `"horizontal"` |
| [`div` props](#tabs-div-props)                   |                                    | false    |                |

##### Tabs `as`

`as?: keyof JSX.IntrinsicElements | React.ComponentType`

Tabs will render a `div` unless you specify a different element.

##### Tabs `children`

`children: React.ReactNode | ((props: { focusedIndex: number; id: string; selectedIndex: number }) => React.ReactNode)`

Tabs expects `<TabList>` and `<TabPanels>` as children. The order doesn't matter; you can have tabs on the top or the bottom. You can have random elements inside as well.

```jsx
// jsx-demo
function Example() {
	return (
		<Tabs>
			<div>Random</div>
			<TabPanels>
				<TabPanel>Uno</TabPanel>
				<TabPanel>Dos</TabPanel>
			</TabPanels>
			<TabList>
				<Tab>Uno</Tab>
				<Tab>Dos</Tab>
			</TabList>
		</Tabs>
	);
}
```

You can also pass a render function to access data relevant to nested components.

```jsx
function Example() {
	return (
		<Tabs keyboardActivation="manual">
			{({ selectedIndex, focusedIndex }) => {
				let getTabStyle = (index) => ({
					borderBottom: `4px solid ${
						selectedIndex === index
							? "red"
							: focusedIndex === index
							? "blue"
							: "black"
					}`,
				});
				return (
					<React.Fragment>
						<TabList>
							<Tab style={getTabStyle(0)}>Uno</Tab>
							<Tab style={getTabStyle(1)}>Dos</Tab>
							<Tab style={getTabStyle(2)}>Tres</Tab>
						</TabList>
						<TabPanels>
							<TabPanel>Uno</TabPanel>
							<TabPanel>Dos</TabPanel>
							<TabPanel>Tres</TabPanel>
						</TabPanels>
					</React.Fragment>
				);
			}}
		</Tabs>
	);
}
```

##### Tabs `defaultIndex`

`defaultIndex?: number`

Starts the tabs at a specific index.

```jsx
// jsx-demo
function Example() {
	return (
		<Tabs defaultIndex={1}>
			<TabPanels>
				<TabPanel>
					<img src="https://placekitten.com/400/200" alt="A picture of a cat" />
				</TabPanel>
				<TabPanel>
					<img
						src="https://www.placecage.com/400/200"
						alt="A picture of Nicolas Cage"
					/>
				</TabPanel>
			</TabPanels>
			<TabList>
				<Tab>Kitten</Tab>
				<Tab>Cage</Tab>
			</TabList>
		</Tabs>
	);
}
```

##### Tabs `index`

`index?: number`

Like form inputs, a tab's state can be controlled by the owner. Make sure to include an `onChange` as well, or else the tabs will not be interactive.

```jsx
// jsx-demo
function Example() {
	const [tabIndex, setTabIndex] = React.useState(0);

	const handleSliderChange = (event) => {
		setTabIndex(parseInt(event.target.value, 10));
	};

	const handleTabsChange = (index) => {
		setTabIndex(index);
	};

	return (
		<div>
			<input
				type="range"
				min="0"
				max="2"
				value={tabIndex}
				onChange={handleSliderChange}
			/>

			<Tabs index={tabIndex} onChange={handleTabsChange}>
				<TabList>
					<Tab>One</Tab>
					<Tab>Two</Tab>
					<Tab>Three</Tab>
				</TabList>
				<TabPanels>
					<TabPanel>
						<p>Click the tabs or pull the slider around</p>
					</TabPanel>
					<TabPanel>
						<p>Yeah yeah. What's up?</p>
					</TabPanel>
					<TabPanel>
						<p>Oh, hello there.</p>
					</TabPanel>
				</TabPanels>
			</Tabs>
		</div>
	);
}
```

##### Tabs `keyboardActivation`

`keyboardActivation?: TabsKeyboardActivation`

Describes the activation mode when navigating a tablist with a keyboard. When set to `"auto"` (`TabsKeyboardActivation.Auto`), a tab panel is activated automatically when a tab is highlighted using arrow keys. When set to `"manual"` (`TabsKeyboardActivation.Manual`), the user must activate the tab panel with either the <kbd>Spacebar</kbd> or <kbd>Enter</kbd> keys. Defaults to `"auto"`.

> **NOTE:** TypeScript users should import and use the `TabsKeyboardActivation` enum when used in strict mode.

```jsx
import { TabsKeyboardActivation } from "@reach/tabs";

function MyTabs() {
	return (
		<Tabs keyboardActivation={TabsKeyboardActivation.Manual}>{/* ... */}</Tabs>
	);
}
```

##### Tabs `onChange`

`onChange?: (index: number) => void`

Calls back with the tab index whenever the user changes tabs, allowing your app to synchronize with it.

```jsx
// jsx-demo
function Example() {
	const colors = ["firebrick", "goldenrod", "dodgerblue"];
	const [tabIndex, setTabIndex] = React.useState(0);
	const backgroundColor = colors[tabIndex];
	return (
		<Tabs
			onChange={(index) => setTabIndex(index)}
			style={{
				color: "white",
				background: backgroundColor,
			}}
		>
			<TabList>
				<Tab>Red</Tab>
				<Tab>Yellow</Tab>
				<Tab>Blue</Tab>
			</TabList>
			<TabPanels style={{ padding: 20 }}>
				<TabPanel>The Primary Colors</TabPanel>
				<TabPanel>Are 1, 2, 3</TabPanel>
				<TabPanel>Red, yellow and blue.</TabPanel>
			</TabPanels>
		</Tabs>
	);
}
```

##### Tabs `orientation`

`orientation?: TabsOrientation`

Allows you to switch the orientation of the tabs relative to their tab panels. This value can either be `"horizontal"` (`TabsOrientation.Horizontal`) or `"vertical"` (`TabsOrientation.Vertical`). Defaults to `"horizontal"`.

Changing the orientation will change how the arrow keys navigate between tabs. Arrow key navigation should logically follow the order in which tabs appear on the screen. For screen reader users, the `aria-orientation` attribute provides the appropriate context to direct which keys should navigate to the next tab (this is provided automatically). As such, it's important to use this prop even if you have already styled your tabs for vertical layout.

```jsx
// jsx-demo
function Example() {
	// Try changing the orientation!
	return (
		<Tabs orientation="vertical">
			<TabList>
				<Tab>One</Tab>
				<Tab>Two</Tab>
				<Tab>Three</Tab>
			</TabList>

			<TabPanels>
				<TabPanel>
					<p>one!</p>
				</TabPanel>
				<TabPanel>
					<p>two!</p>
				</TabPanel>
				<TabPanel>
					<p>three!</p>
				</TabPanel>
			</TabPanels>
		</Tabs>
	);
}
```

> **NOTE:** TypeScript users should import and use the `TabsOrientation` enum when used in strict mode.

```jsx
import { TabsOrientation } from "@reach/tabs";

function MyTabs() {
	return <Tabs orientation={TabsOrientation.Vertical}>{/* ... */}</Tabs>;
}
```

##### Tabs `div` props

All other props are passed to the underlying `div` (or another component passed to the `as` prop).

### TabList

The parent component of the tabs.

```jsx
<TabList>
	<Tab>Tacos</Tab>
	<Tab>Tortas</Tab>
</TabList>
```

#### TabList CSS Selectors

Please see the [styling guide](/styling).

```css
[data-reach-tab-list] {
}
```

#### TabList Props

| Prop                              | Type                          | Required | Default |
| --------------------------------- | ----------------------------- | -------- | ------- |
| [`as`](#tablist-as)               | `string` <Pipe /> `Component` | false    | `"div"` |
| [`children`](#tablist-children)   | node                          | true     |         |
| [`div` props](#tablist-div-props) |                               |          |         |

##### TabList `children`

`children: React.ReactNode`

`TabList` expects multiple `Tab` elements as children.

```jsx
<TabList>
	<Tab>One</Tab>
	<Tab>Two</Tab>
</TabList>
```

But, you can also wrap `Tab` as long as you forward the props (because data is passed from `TabList` to `Tab` via React context).

```jsx
const RedTab = (props) => <Tab {...props} style={{ color: "red" }} />;

const TabPage = () => (
	<Tabs>
		<TabList>
			<RedTab>This is red</RedTab>
			<Tab>This is normal</Tab>
		</TabList>
		<TabPanels>
			<TabPanel>...</TabPanel>
			<TabPanel>...</TabPanel>
		</TabPanels>
	</Tabs>
);
```

##### TabList `as`

`as?: keyof JSX.IntrinsicElements | React.ComponentType`

Tabs will render a `div` unless you specify a different element.

```jsx
<TabList as={View} />
```

##### TabList `div` props

All other props are passed to the underlying `div` (or component passed to `as`).

### TabPanels

The parent component of the panels.

```jsx
<TabPanels>
	<TabPanel>My favorite</TabPanel>
	<TabPanel>My other favorite</TabPanel>
</TabPanels>
```

#### TabPanels CSS Selectors

Please see the [styling guide](/styling).

```css
[data-reach-tab-panels] {
}
```

#### TabPanels Props

| Prop                                | Type                          | Required | Default |
| ----------------------------------- | ----------------------------- | -------- | ------- |
| [`as`](#tabpanels-as)               | `string` <Pipe /> `Component` | false    | `"div"` |
| [`children`](#tabpanels-children)   | `node`                        | true     |         |
| [`div` props](#tabpanels-div-props) |                               |          |         |

##### TabPanels `as`

`as?: keyof JSX.IntrinsicElements | React.ComponentType`

Tabs will render a `div` unless you specify a different element.

```jsx
<TabPanels as={View} />
```

##### TabPanels `children`

`children: React.ReactNode`

`TabPanels` expects multiple `TabPanel` elements as children.

```jsx
<TabPanels>
	<TabPanel>One</TabPanel>
	<TabPanel>Two</TabPanel>
</TabPanels>
```

But, you can also wrap `TabPanel` as long as you forward the props (because data is passed from `TabPanels` to `TabPanel` via React context`).

```jsx
const BoldPanel = (props) => (
	<TabPanel {...props} style={{ fontWeight: "bold" }} />
);

const TabPage = () => (
	<Tabs>
		<TabList>
			<Tab>...</Tab>
			<Tab>...</Tab>
		</TabList>
		<TabPanels>
			<BoldPanel>...</BoldPanel>
			<TabPanel>...</TabPanel>
		</TabPanels>
	</Tabs>
);
```

##### TabPanels `div` props

All other props are passed to the underlying `div` (or component passed to `as`).

### Tab

The interactive element that changes the selected panel.

```jsx
<Tab>Coconut Korma</Tab>
```

#### Tab CSS Selectors

Please see the [styling guide](/styling).

```css
/* styles all tabs */
[data-reach-tab] {
}

/* styles only the selected tab */
[data-reach-tab][data-selected] {
}
```

#### Tab Props

| Prop                                | Type                          | Required | Default    |
| ----------------------------------- | ----------------------------- | -------- | ---------- |
| [`as`](#tab-as)                     | `string` <Pipe /> `Component` | false    | `"button"` |
| [`children`](#tab-children)         | node                          | true     |            |
| [`disabled`](#tab-disabled)         | boolean                       | false    | `false`    |
| [`button` props](#tab-button-props) |                               |          |            |

##### Tab `as`

`as?: keyof JSX.IntrinsicElements | React.ComponentType`

Tab will render a `button` unless you specify a different element.

```jsx
<Tab as={ReactNativeWebButton} />
```

##### Tab `children`

`children: React.ReactNode`

`Tab` can receive any type of children.

```jsx
<Tab>
	<HouseIcon /> Home
</Tab>
```

##### Tab `disabled`

`disabled?: boolean`

Disables a tab when true. Clicking will not work and keyboard navigation will skip over it.

```jsx
<Tab disabled />
```

##### Tab `button` props

All other props are passed to the underlying `button` (or component passed to `as`).

### TabPanel

The panel that displays when it's corresponding tab is active.

```jsx
<TabPanel>
	<h2>The Best Food</h2>
	<p>The best food is either Mexican or Indian.</p>
</TabPanel>
```

#### TabPanel CSS Selectors

Please see the [styling guide](/styling).

```css
/* styles all tabs */
[data-reach-tab-panel] {
}
```

#### TabPanel Props

| Prop                               | Type                          | Required | Default |
| ---------------------------------- | ----------------------------- | -------- | ------- |
| [`as`](#tabpanel-as)               | `string` <Pipe /> `Component` | false    | `"div"` |
| [`children`](#tabpanel-children)   | `node`                        | false    |         |
| [`div` props](#tabpanel-div-props) |                               |          |         |

##### TabPanel `as`

`as?: keyof JSX.IntrinsicElements | React.ComponentType`

TabPanel will render a `div` unless you specify a different element.

```jsx
<Tab as={View} />
```

##### TabPanel `children`

`children?: React.ReactNode`

`TabPanel` can receive any type of children.

```jsx
<TabPanel>
	<h2>Whatever you want</h2>
	<p>In here</p>
</TabPanel>
```

##### TabPanel `div` props

All other props are passed to the underlying `div` (or component passed to `as`).

### useTabsContext

`function useTabsContext(): { focusedIndex: number; id: string; selectedIndex: number }`

A hook that exposes data for a given `Tabs` component to its descendants.

```jsx
// jsx-demo
(() => {
	function CustomTab({ index, ...props }) {
		const { selectedIndex, focusedIndex } = useTabsContext();
		return (
			<Tab
				style={{
					borderBottom: `4px solid ${
						selectedIndex === index
							? "red"
							: focusedIndex === index
							? "blue"
							: "black"
					}`,
				}}
				{...props}
			/>
		);
	}

	return (
		<Tabs keyboardActivation="manual">
			<TabList>
				<CustomTab index={0}>Uno</CustomTab>
				<CustomTab index={1}>Dos</CustomTab>
				<CustomTab index={2}>Tres</CustomTab>
			</TabList>
			<TabPanels>
				<TabPanel>
					<p>Uno</p>
				</TabPanel>
				<TabPanel>
					<p>Dos</p>
				</TabPanel>
				<TabPanel>
					<p>Tres</p>
				</TabPanel>
			</TabPanels>
		</Tabs>
	);
})();
```

## Demos

These demos show off how you can add quite a bit of behavior to your Tabs interfaces.

### Data-driven tabs

If you'd like to drive your tabs with data you can create a `DataTabs` component.

```jsx
// jsx-demo
(() => {
	function DataTabs({ data }) {
		return (
			<Tabs>
				<TabList>
					{data.map((tab, index) => (
						<Tab key={index}>{tab.label}</Tab>
					))}
				</TabList>
				<TabPanels>
					{data.map((tab, index) => (
						<TabPanel key={index}>{tab.content}</TabPanel>
					))}
				</TabPanels>
			</Tabs>
		);
	}

	// now if you have an array of data...
	const tabData = [
		{ label: "Taco", content: "Perhaps the greatest dish ever invented." },
		{
			label: "Burrito",
			content:
				"Perhaps the greatest dish ever invented but bigger and with rice.",
		},
	];

	// you can just pass it in:
	return <DataTabs data={tabData} />;
})();
```

### Animation

With a little composition we can animate the selected tab bar.

```jsx
// jsx-demo
(() => {
	const HORIZONTAL_PADDING = 8;
	const AnimatedContext = React.createContext();

	function AnimatedTabs({ color, children, ...rest }) {
		// some state to store the position we want to animate to
		const [activeRect, setActiveRect] = React.useState(null);
		const ref = React.useRef();
		const rect = useRect(ref);

		return (
			// put the function to change the styles on context so an active Tab
			// can call it, then style it up
			<AnimatedContext.Provider value={setActiveRect}>
				{/* make sure to forward props since we're wrapping Tabs */}
				<Tabs
					{...rest}
					ref={ref}
					style={{ ...rest.style, position: "relative" }}
				>
					<div
						style={{
							position: "absolute",
							height: 2,
							background: color,
							transition: "all 300ms ease",
							left:
								(activeRect && activeRect.left) -
								(rect && rect.left) +
								HORIZONTAL_PADDING,
							top: (activeRect && activeRect.bottom) - (rect && rect.top),
							// subtract both sides of horizontal padding to center the div
							width: activeRect && activeRect.width - HORIZONTAL_PADDING * 2,
						}}
					/>
					{children}
				</Tabs>
			</AnimatedContext.Provider>
		);
	}

	function AnimatedTab({ index, ...props }) {
		// get the currently selected index from useTabsContext
		const { selectedIndex } = useTabsContext();
		const isSelected = selectedIndex === index;

		// measure the size of our element, only listen to rect if active
		const ref = React.useRef();
		const rect = useRect(ref, { observe: isSelected });

		// get the style changing function from context
		const setActiveRect = useContext(AnimatedContext);

		// callup to set styles whenever we're active
		React.useLayoutEffect(() => {
			if (isSelected) {
				setActiveRect(rect);
			}
		}, [isSelected, rect, setActiveRect]);

		return (
			<Tab
				ref={ref}
				{...props}
				style={{
					...props.style,
					border: "none",
					padding: `4px ${HORIZONTAL_PADDING}px`,
				}}
			/>
		);
	}

	return (
		<AnimatedTabs color="red" style={{ width: 400 }}>
			<TabList style={{ justifyContent: "space-around" }}>
				<AnimatedTab index={0} style={{ flex: 1 }}>
					The First
				</AnimatedTab>
				<AnimatedTab index={1} style={{ flex: 2 }}>
					This has longer text
				</AnimatedTab>
				<AnimatedTab index={2} style={{ flex: 1 }}>
					Three
				</AnimatedTab>
			</TabList>
			<TabPanels style={{ padding: 10 }}>
				<TabPanel>
					<p>Check it out! It's ~animated~</p>
				</TabPanel>
				<TabPanel>
					<p>Yeah yeah. What's up?</p>
				</TabPanel>
				<TabPanel>
					<p>Oh, hello there.</p>
				</TabPanel>
			</TabPanels>
		</AnimatedTabs>
	);
})();
```

### Keyboard Accessibility

| Key                                          | Action                                                                           |
| -------------------------------------------- | -------------------------------------------------------------------------------- |
| <kbd>Enter</kbd> / <kbd>Spacebar</kbd>       | Sets the focused tab to `active` when `keyboardActivation` is set to `"manual"`. |
| <kbd>ArrowUp</kbd> / <kbd>ArrowDown</kbd>    | Navigates between tabs when `orientation` is `"vertical"`.                       |
| <kbd>ArrowLeft</kbd> / <kbd>ArrowRight</kbd> | Navigates between tabs when `orientation` is `"horizontal"`.                     |
| <kbd>Home</kbd> / <kbd>PageUp</kbd>          | Navigates to the last tab in the `TabList`.                                      |
| <kbd>End</kbd> / <kbd>PageDown</kbd>         | Navigates to the first tab in the `TabList`.                                     |
